基本概念

基本概念

编程技术的基本概念

Python 是动态编译的语言,和 C/C++、Java 或者 Kotlin 等静态语言不同,它是在运行时一句一句代码地边编译边执行的,而 Java 是提前将高级语言编译成了 JVM 字节码,运行时直接通过 JVM 和机器打交道,所以进行密集计算时运行速度远高于动态编译语言。为了解决编译的性能问题,Python 的一个实现 PyPy 使用了 JIT(即时编译)技术,混合了动态编译和静态编译的特性,仍然是一句一句编译源代码,但是会将翻译过的代码缓存起来以降低性能损耗。相对于静态编译代码,即时编译的代码可以处理延迟绑定并增强安全性。

JIT 编译

参考博客:什么是JIT,写的很好 - ddzh2020 - 博客园

  1. 动态编译(dynamic compilation)指的是“在运行时进行编译”;与之相对的是事前编译(ahead-of-time compilation,简称 AOT),也叫静态编译(static compilation)。

  2. JIT编译(just-in-time compilation)狭义来说是当某段代码即将第一次被执行时进行编译,因而叫“即时编译”。JIT 编译是动态编译的一种特例。JIT 编译一词后来被泛化,时常与动态编译等价;但要注意广义与狭义的 JIT 编译所指的区别。

  3. 自适应动态编译(adaptive dynamic compilation)也是一种动态编译,但它通常执行的时机比 JIT 编译迟,先让程序“以某种式”先运行起来,收集一些信息之后再做动态编译。这样的编译可以更加优化。

JIT 在 JVM 中的使用

Java技术专区-彻底你明白什么是JIT编译器(Just In Time编译器)_51CTO博客_Jit编译器

在部分商用虚拟机中(如 HotSpot),Java 程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,下文统称 JIT 编译器)。

即时编译器并不是虚拟机必须的部分,Java 虚拟机规范并没有规定 Java 虚拟机内必须要有即时编译器存在,更没有限定或指导即时编译器应该如何去实现。但是,即时编译器编译性能的好坏、代码优化程度的高低却是衡量一款商用虚拟机优秀与否的最关键的指标之一,它也是虚拟机中最核心且最能体现虚拟机技术水平的部分。

由于 Java 虚拟机规范并没有具体的约束规则去限制即使编译器应该如何实现,所以这部分功能完全是与虚拟机具体实现相关的内容,如无特殊说明,我们提到的编译器、即时编译器都是指 Hotspot 虚拟机内的即时编译器,虚拟机也是特指 HotSpot 虚拟机。

尽管并不是所有的 Java 虚拟机都采用解释器与编译器并存的架构,但许多主流的商用虚拟机(如 HotSpot),都同时包含解释器和编译器。解释器与编译器两者各有优势:当程序需要 * 迅速启动和执行 * 的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取 * 更高的执行效率*。当程序运行环境中内存资源限制较大(如部分嵌入式系统中),可以使用解释器执行节约内存,反之可以使用编译执行来提升效率。此外,如果编译后出现“罕见陷阱”,可以通过逆优化退回到解释执行。

为什么解释器比编译器快,具体请看下一小节 编译的时间开销

HotSpot 虚拟机中内置了两个即时编译器:Client ComplierServer Complier,简称为 C1、C2 编译器,分别用在客户端和服务端。目前主流的 HotSpot 虚拟机中默认是采用解释器与其中一个编译器直接配合的方式工作。程序使用哪个编译器,取决于虚拟机运行的模式。HotSpot 虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用“-client”或“-server”参数去强制指定虚拟机运行在 Client 模式或 Server 模式。

这两个编译器有什么区别呢?

Server Compiler 和 Client Compiler 两个编译器的编译过程是不一样的。

对 Client Compiler 来说,它是一个简单快速的编译器,主要关注点在于局部优化,而放弃许多耗时较长的全局优化手段。

而 Server Compiler 则是专门面向服务器端的,并为服务端的性能配置特别调整过的编译器,是一个充分优化过的高级编译器。

为什么提供多个即时编译器与为什么提供多个垃圾收集器类似,都是为了适应不同的应用场景。

编译的时间开销

解释器的执行,抽象的看是这样的:

输入的代码 -> [ 解释器 解释执行 ] -> 执行结果

而要 JIT 编译然后再执行的话,抽象的看则是:

输入的代码 -> [ 编译器 编译 ] -> 编译后的代码 -> [ 执行 ] -> 执行结果

说 JIT 比解释快,其实说的是“执行编译后的代码”比“解释器解释执行”要快,并不是说“编译”这个动作比“解释”这个动作快。
JIT 编译再怎么快,至少也比解释执行一次略慢一些,而要得到最后的执行结果还得再经过一个“执行编译后的代码”的过程。
所以,对“只执行一次”的代码而言,解释执行其实总是比 JIT 编译执行要快。
怎么算是“只执行一次的代码”呢?粗略说,下面两个条件同时满足时就是严格的“只执行一次”
1、只被调用一次,例如类的构造器(class initializer<clinit>()
2、没有循环
对只执行一次的代码做 JIT 编译再执行,可以说是得不偿失。
对只执行少量次数的代码,JIT 编译带来的执行速度的提升也未必能抵消掉最初编译带来的开销。

只有对频繁执行的代码,JIT 编译才能保证有正面的收益。

编译的空间开销

对一般的 Java 方法而言,编译后代码的大小相对于字节码的大小,膨胀比达到 10x 是很正常的。同上面说的时间开销一样,这里的空间开销也是,只有对执行频繁的代码才值得编译,如果把所有代码都编译则会显著增加代码所占空间,导致“代码爆炸”。
这也就解释了为什么有些 JVM 会选择不总是做 JIT 编译,而是选择用解释器 +JIT 编译器的混合执行引擎